package com.guang.config;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.TreeCache;
import org.apache.curator.framework.recipes.cache.TreeCacheEvent;
import org.apache.curator.framework.recipes.cache.TreeCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.context.support.XmlWebApplicationContext;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 配置中心工具类
 * 将数据源信息加载到配置中心中去，多个服务提供者不需要进行重新，就可以来进行动态刷新功能
 *          利用到的原理是：zookeeper的通知机制！可以来实现注册中心和配置中心
 * PropertyPlaceholderConfigurer：== context:property-placeholder 加载jdbc.properties类
 */
public class DynamicDataSourceRefreshFromZKConfig extends PropertyPlaceholderConfigurer implements ApplicationContextAware {

    private XmlWebApplicationContext xmlWebApplicationContext;

    private static ConcurrentHashMap<String,String> dataSourceInformationMap = new ConcurrentHashMap<>(128);
// static代码块和statis修饰的方法都将会加载，防止加载两次，所以这次替换掉
//    static {
//        loadProps();
//    }

    private static synchronized  void loadProps() {
        System.out.println("开始加载properties文件内容.......");
        Properties properties = new Properties();
        try (InputStream in = DynamicDataSourceRefreshFromZKConfig.class.getClassLoader().getResourceAsStream("jdbc.properties")) {
            properties.load(in);
            for (String propertyName : properties.stringPropertyNames()) {
                dataSourceInformationMap.put(propertyName,properties.getProperty(propertyName));
            }
        } catch (FileNotFoundException e) {
            System.out.println("jdbc.properties文件未找到");
            System.out.println(e);
        } catch (IOException e) {
            System.out.println("加载jdbc.properties出现IOException");
            System.out.println(e);
        }
        System.out.println("加载properties文件内容完成...........");
    }

    /**
     * 重写父类方法 加载配置数据
     * @param beanFactoryToProcess
     * @param props
     * @throws BeansException
     */
    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {
        //1.从zk中获取配置数据放到props对象中
        loadZk(props);
        //2.添加监听
        addWatch(props);
        //2.调用父类
        super.processProperties(beanFactoryToProcess, props);
    }

    /**
     * 添加监听
     * @param props
     */
    private void addWatch(Properties props) {
        //1.创建重试对象
        try {
            /**
             * 三秒之后尝试刷新
             * 最大重试三次
             */
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,3);
            //2.创建客户端对象
            CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181",retryPolicy);
            //3.开启客户端
            client.start();
            //4.创建监听对象
            dataSourceInformationMap.forEach((propertyName,propertyValue)->{
                TreeCache treeCache = new TreeCache(client,"/"+propertyName);
                try {
                    treeCache.start();//一定要开启监听
                    System.out.println("对于节点/"+propertyName+"开启监听成功");
                } catch (Exception e) {
                    System.out.println("对于节点/"+propertyName+"开启监听失败");
                }
                treeCache.getListenable().addListener(new TreeCacheListener() {
                    @Override
                    public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
                        if(event.getType() == TreeCacheEvent.Type.NODE_UPDATED){
                            //这里不需要重新加载，因为重新刷新的时候，还是会调用到processProperties方法中来，然后重新从zk中来加载数据
                            // 所以最佳实践是直接刷新容器
                            xmlWebApplicationContext.refresh();
                        }
                    }
                });
            });
        } catch (Exception e) {
            System.out.println("监听失败。。。。。");
            e.printStackTrace();
        }

    }

    /**
     * 从zk中获取配置数据放到props对象中
     * @param props
     */
    private void loadZk(Properties props) {
        try {
            Thread refreshDataSourceThread = new Thread(() -> {
                synchronized (DynamicDataSourceRefreshFromZKConfig.class){
                    loadProps();
                }
            }, "RefreshDataSource-Thread");
            refreshDataSourceThread.start();
            //1.创建重试对象
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,3);
            //2.创建客户端对象
            CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181",retryPolicy);
            //3.开启客户端
            client.start();
            /**
             * TODO 其实这里也可以修改！因为不同项目的数据源不同，加载的数据源前缀也是不同的，所以可以将其改成从一个固定的文件中来进行读取即可
             *      因为就其设计角度来说，肯定是先从配置文件加载！然后在线上运行阶段发现数据库无法支撑，才去ZK中改的。但是第一次的话，是从配置中间中来进行加载的
             *      这块先在这里来进行预留，后期有时间我会回头来给这里补上对应的代码
             */
            refreshDataSourceThread.join();
            // 向ZK中注册节点信息
            registNodeForZK(dataSourceInformationMap,client);
            getNodeFromZK(client,props);
            client.close();
        } catch (Exception e) {
            System.out.println("加载zk配置文件失败了。。。。。。");
            e.printStackTrace();
        }
    }

    private void getNodeFromZK( CuratorFramework client, Properties props) {
        dataSourceInformationMap.forEach((propertyName,propertyValue)->{
            try {
                String nodeValue  = new String(client.getData().forPath("/"+propertyName));
                props.setProperty(propertyName,nodeValue);
            } catch (Exception e) {
                System.out.println("将当前值放入到对应的值中为空");
            }
        });
    }


    private void registNodeForZK(ConcurrentHashMap<String,String> dataSourceInformationMap, CuratorFramework client) {
        dataSourceInformationMap.forEach((propertyName,propertyValue)->{
            try {
                String createNodeResult = client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/" + propertyName, propertyValue.getBytes(StandardCharsets.UTF_8));
                System.out.println("创建节点对应的信息是："+createNodeResult);
            } catch (Exception e) {
                System.out.println("创建节点/"+propertyName+"失败，对应的值是："+propertyValue);
            }
        });

    }


    /**
     * 启动的时候 将spring容器赋值给当前类的对象
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        xmlWebApplicationContext = (XmlWebApplicationContext)applicationContext;
    }
}